How do I update a specific edge property using Gremlin/Titan/TinkerPop3?

Jason already answered nearly all of your questions. The only aspect missing is:

If the edge does not exist, it should be created.

So I'll try to answer this point with a slightly different query. This query adds a new edge if it doesn't exist already and then updates / adds the weight property:

g.V(vertex.id()).outE('votes_for').has('type', 'eat')
    .where(__.inV().hasLabel('meal').has('name','pizza')) // filter for the edge to update
    .tryNext()                                            // select the edge if it exists
    .orElseGet({g.V(vertex.id()).next()
        .addEdge('votes_for', g.V(pizzaId).next(), 'type', 'eat')}) // otherwise, add the edge
    .property('weight', 0.99)               // finally, update / add the 'weight' property

You are on the right track. Check out the vertex steps documentation.

Label the edge, then traverse from the edge to the vertex to check, then jump back to the edge to update the property.

g.V(vertex.id()).
  outE("votes_for").has("type", "eat").as("e").
  inV().has("name", "pizza").
  select("e").property("weight", 0.99d).
  iterate()

Full Gremlin console session:

gremlin> Titan.version()
==>1.0.0
gremlin> Gremlin.version()
==>3.0.1-incubating
gremlin> graph = TitanFactory.open('inmemory'); g = graph.traversal()
==>graphtraversalsource[standardtitangraph[inmemory:[127.0.0.1]], standard]
gremlin> vertex = graph.addVertex(T.label, 'user', 'given_name', 'Joey', 'family_name', 'Tribbiani')
==>v[4200]
gremlin> pizza = graph.addVertex(T.label, 'meal', 'name', 'pizza')
==>v[4104]
gremlin> votes = vertex.addEdge('votes_for', pizza, 'type', 'eat', 'weight', 0.8d)
==>e[1zh-38o-4r9-360][4200-votes_for->4104]
gremlin> g.E(votes).valueMap(true)
==>[label:votes_for, weight:0.8, id:2rx-38o-4r9-360, type:eat]
gremlin> g.V(vertex.id()).outE('votes_for').has('type','eat').as('e').inV().has('name','pizza').select('e').property('weight', 0.99d).iterate(); g.E(votes).valueMap(true)
==>[label:votes_for, weight:0.99, id:2rx-38o-4r9-360, type:eat]

Would explicitly specifying a Titan schema help?

If you wanted to start from the Joey node without having a reference to the vertex or its id, this would be a good use case for a Titan composite index. The traversal would start with:

g.V().has("given_name", "Joey")

Are there any helper/utility methods I don't know of?

In addition to the TinkerPop reference documentation, there are several tutorials that you can read through:

  1. Getting Started
  2. The Gremlin Console
  3. Recipes

Would it make more sense to have several vote_for labels instead of one label + type property, like vote_for_eat?

Depends on what your graph model or query patterns are, but more granular labels like vote_for_eat can work out fine. You can pass multiple edge labels on the traversal step:

g.V(vertex.id()).outE('vote_for_eat', 'vote_for_play', 'vote_for_sleep')

Update

There may only exist at most one edge of the same type between the two nodes

You can use the Titan schema to help with this, specifically define an edge label with multiplicity ONE2ONE. An exception will be thrown if you create more than one votes_for_eat between Joey and pizza.