Here's an attempt which tries to avoid killing a process after it has already exited, which reduces the chance of killing another process with the same process ID (although it's probably impossible to avoid this kind of error completely).
run_with_timeout ()
{
  t=$1
  shift
  echo "running \"$*\" with timeout $t"
  (
  # first, run process in background
  (exec sh -c "$*") &
  pid=$!
  echo $pid
  # the timeout shell
  (sleep $t ; echo timeout) &
  waiter=$!
  echo $waiter
  # finally, allow process to end naturally
  wait $pid
  echo $?
  ) \
  | (read pid
     read waiter
     if test $waiter != timeout ; then
       read status
     else
       status=timeout
     fi
     # if we timed out, kill the process
     if test $status = timeout ; then
       kill $pid
       exit 99
     else
       # if the program exited normally, kill the waiting shell
       kill $waiter
       exit $status
     fi
  )
}
Use like run_with_timeout 3 sleep 10000, which runs sleep 10000 but ends it after 3 seconds.
This is like other answers which use a background timeout process to kill the child process after a delay. I think this is almost the same as Dan's extended answer (https://stackoverflow.com/a/5161274/1351983), except the timeout shell will not be killed if it has already ended.
After this program has ended, there will still be a few lingering "sleep" processes running, but they should be harmless.
This may be a better solution than my other answer because it does not use the non-portable shell feature read -t and does not use pgrep.